package directory

import (
	"bytes"
	"context"
	"encoding/base64"
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"

	"mall/common/globalkey"
	"mall/common/modelx"
	"mall/common/utils/pointy"
	"mall/service/attachment/model"
	"mall/service/attachment/rpc/internal/svc"
	"mall/service/attachment/rpc/internal/utils/errorhandler"
	"mall/service/attachment/rpc/types/attachment"
	"mall/service/backend/common/errorx"
	"mall/service/backend/common/i18n"

	"github.com/qiniu/go-sdk/v7/auth/qbox"
	"github.com/qiniu/go-sdk/v7/storage"
	"github.com/sirupsen/logrus"
	"github.com/volatiletech/null/v8"
	"github.com/zeromicro/go-zero/core/logx"
)

// https://developer.qiniu.com/kodo/1238/go#8
//   Go SDK
// https://developer.qiniu.com/dora/1279/basic-processing-images-imageview2
//   图片基本处理（imageView2）
// https://developer.qiniu.com/dora/1316/image-watermarking-processing-watermark
//   图片水印处理（watermark）
// https://developer.qiniu.com/dora/5915/blind-watermarking-processing
//   盲水印处理（watermark）

type BackendCreateRecordLogic struct {
	ctx           context.Context
	svcCtx        *svc.ServiceContext
	bucketManager *storage.BucketManager
	bucket        string
	config        *storage.Config
	logx.Logger
}

func NewBackendCreateRecordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BackendCreateRecordLogic {
	return &BackendCreateRecordLogic{
		ctx:    ctx,
		svcCtx: svcCtx,
		Logger: logx.WithContext(ctx),
	}
}

func (l *BackendCreateRecordLogic) BackendCreateRecord(in *attachment.RecordInfo) (*attachment.RecordInfo, error) {
	var diagnose uint32
	if in.Diagnose != nil {
		diagnose = *in.Diagnose
	}

	mac := qbox.NewMac(l.svcCtx.Config.QiniuConf.AccessKey, l.svcCtx.Config.QiniuConf.SecretKey)

	cfg := storage.Config{
		UseHTTPS:      l.svcCtx.Config.QiniuConf.UseHTTPS,      // 是否使用https域名进行资源管理
		UseCdnDomains: l.svcCtx.Config.QiniuConf.UseCdnDomains, // 上传是否使用CDN上传加速
	}
	// 指定空间所在的区域，如果不指定将自动探测
	// 如果没有特殊需求，默认不需要指定
	// cfg.Region=&storage.ZoneHuabei
	switch l.svcCtx.Config.QiniuConf.Region {
	case "z2":
		cfg.Region = &storage.ZoneHuanan
	}

	l.bucketManager = storage.NewBucketManager(mac, &cfg)
	l.bucket = l.svcCtx.Config.QiniuConf.Bucket
	l.config = &cfg

	var res *model.AttachmentFileInfo
	var err error
	switch diagnose {
	case 1: // 抓取网络资源到空间 指定保存的key
		res, err = l.qiniuFetchWithoutKey(in)
	case 11: // 文件上传（表单方式）
		res, err = l.qiniuPutFile(in)
	case 12: // 字节数组上传（表单方式）
		res, err = l.qiniuPut(in)
	case 13: // 数据流上传（表单方式） 文件分片上传
		res, err = l.qiniuResumePut(in)
	case 14: // 数据流上传（表单方式） 文件断点续传
		res, err = l.qiniuResumePutFile(in)
	case 21: // 持久化数据处理 - 生成缩略图
		res, err = l.qiniuPfopThumbnail(in)
	case 22: // 持久化数据处理 - 加水印
		res, err = l.qiniuPfopWatermark(in)
	case 31: // Prefop 持久化处理状态查询
		res, err = l.qiniuPrefop(in)
	case 32: // Prefop 持久化处理状态查询
		res, err = l.qiniuPfopCallback(in)
	default: // 抓取网络资源到空间 不指定保存的key，默认用文件hash作为文件名
		res, err = l.qiniuFetch(in)
	}

	if err != nil {
		return nil, err
	}

	if res == nil {
		return &attachment.RecordInfo{}, nil
	}

	var updatedAt *string
	if res.PutTime > 0 {
		updatedAt = pointy.GetPointer(modelx.FormatTime(time.Unix(0, int64(res.PutTime)*100), ""))
	}
	var expiredAt *string
	if res.Expiration > 0 {
		expiredAt = pointy.GetPointer(modelx.FormatTime(time.Unix(int64(res.Expiration), 0), ""))
	}
	var meta *string
	if res.MetaValue.Valid {
		meta = pointy.GetPointer(res.MetaValue.String)
	}

	return &attachment.RecordInfo{
		Id:            pointy.GetPointer(strconv.FormatUint(res.ID, 10)),
		FileId:        &res.AttachmentID,
		ParentId:      &res.ParentID,
		Bucket:        &res.Bucket,
		Key:           &res.KeyValue,
		Size:          &res.Size,
		Hash:          &res.Hash,
		MimeType:      &res.MimeType,
		Type:          pointy.GetPointer(uint32(res.Type)),
		Status:        pointy.GetPointer(uint32(res.Status)),
		RestoreStatus: pointy.GetPointer(uint32(res.RestoreStatus)),
		Md5:           &res.MD5,
		EndUser:       &res.EndUser,
		MetaValue:     meta,
		DelStatus:     pointy.GetPointer(uint32(res.DelStatus)),
		UpdatedAt:     updatedAt,
		ExpiredAt:     expiredAt,
	}, nil
}

func (l *BackendCreateRecordLogic) createOrUpdate(
	bucket string,
	key string,
	data *model.AttachmentFileInfo,
) (*model.AttachmentFileInfo, error) {
	fileInfo, err := l.bucketManager.Stat(bucket, key)
	if err != nil {
		logrus.Info(fmt.Sprintf("Qiniu Stat err: %+v", err))
		// Qiniu Stat err: no such file or directory
		return nil, err
	}

	if data != nil && data.ID > 0 {
		data.KeyValue = key
		data.Bucket = bucket
		data.Size = uint64(fileInfo.Fsize)
		data.Hash = fileInfo.Hash
		data.MimeType = fileInfo.MimeType
		data.Type = uint8(fileInfo.Type)
		data.PutTime = uint64(fileInfo.PutTime)
		data.RestoreStatus = uint8(fileInfo.RestoreStatus)
		data.Status = uint8(fileInfo.Status)
		data.MD5 = fileInfo.Md5
		data.EndUser = fileInfo.EndUser
		data.Expiration = uint64(fileInfo.Expiration)
		data.TransitionToIa = uint64(fileInfo.TransitionToIA)
		data.TransitionToArchive = uint64(fileInfo.TransitionToArchive)
		data.TransitionToDeepArchive = uint64(fileInfo.TransitionToDeepArchive)
		if len(fileInfo.Parts) > 0 {
			var parts []string
			for _, partId := range fileInfo.Parts {
				parts = append(parts, strconv.FormatInt(partId, 10))
			}
			data.Parts = strings.Join(parts, ",")
		}
		if fileInfo.MetaData != nil {
			var items []string
			for mk, mv := range fileInfo.MetaData {
				items = append(items, mk+":"+mv)
			}
			data.MetaValue = null.StringFrom(strings.Join(items, ";"))
		}
		err := l.svcCtx.FileInfo.Update(l.ctx, nil, data)
		if err != nil {
			return nil, err
		}

		return data, nil
	}

	var newData model.AttachmentFileInfo
	newData.KeyValue = key
	newData.Bucket = bucket
	newData.Size = uint64(fileInfo.Fsize)
	newData.Hash = fileInfo.Hash
	newData.MimeType = fileInfo.MimeType
	newData.Type = uint8(fileInfo.Type)
	newData.PutTime = uint64(fileInfo.PutTime)
	newData.RestoreStatus = uint8(fileInfo.RestoreStatus)
	newData.Status = uint8(fileInfo.Status)
	newData.MD5 = fileInfo.Md5
	newData.EndUser = fileInfo.EndUser
	newData.Expiration = uint64(fileInfo.Expiration)
	newData.TransitionToIa = uint64(fileInfo.TransitionToIA)
	newData.TransitionToArchive = uint64(fileInfo.TransitionToArchive)
	newData.TransitionToDeepArchive = uint64(fileInfo.TransitionToDeepArchive)
	if len(fileInfo.Parts) > 0 {
		var parts []string
		for _, partId := range fileInfo.Parts {
			parts = append(parts, strconv.FormatInt(partId, 10))
		}
		newData.Parts = strings.Join(parts, ",")
	}
	if fileInfo.MetaData != nil {
		var items []string
		for mk, mv := range fileInfo.MetaData {
			items = append(items, mk+":"+mv)
		}
		newData.MetaValue = null.StringFrom(strings.Join(items, ";"))
	}
	if data != nil {
		if len(data.PersistentID) > 0 {
			newData.PersistentID = data.PersistentID
		}
		if data.AttachmentID > 0 {
			newData.AttachmentID = data.AttachmentID
		}
		if data.ParentID > 0 {
			newData.ParentID = data.ParentID
		}
	}
	err = l.svcCtx.FileInfo.Insert(l.ctx, nil, &newData)
	if err != nil {
		return nil, err
	}

	return &newData, err
}

func (l *BackendCreateRecordLogic) qiniuFetch(
	in *attachment.RecordInfo,
) (*model.AttachmentFileInfo, error) {
	if in.Url != nil && len(strings.TrimSpace(*in.Url)) > 0 {
		var url string = strings.TrimSpace(*in.Url)
		var key string
		if in.Key != nil && len(strings.TrimSpace(*in.Key)) > 0 {
			key = strings.TrimSpace(*in.Key)
		} else {
			slashIndex := strings.LastIndex(url, "/")
			if slashIndex == -1 {
				logx.Errorw("reject the file which does not have suffix")
				return nil, errorx.NewInvalidArgumentError("file.wrongTypeError")
			}

			key = url[slashIndex+1:]
		}

		// 指定保存的key
		fetchRet, err := l.bucketManager.Fetch(url, l.bucket, key)
		if err != nil {
			logrus.Info(fmt.Sprintf("Qiniu Fetch err: %+v", err))
			return nil, errorx.NewInternalError("file.fetchFileFailed")
		}

		logrus.Info(fmt.Sprintf("Qiniu Fetch: %+v", fetchRet))
		// {Hash:FpHyF0kkil3sp-SaXXX8TBJY3jDh Fsize:163469 MimeType:image/png Key:FpHyF0kkil3sp-SaXXX8TBJY3jDh}
		// {Hash:FpHyF0kkil3sp-SaXXX8TBJY3jDh Fsize:163469 MimeType:image/png Key:/uploads/other/202310/018b46f1-270f-7c27-9bfc-9c844db63e68.png}

		data, err := l.createOrUpdate(l.bucket, key, nil)
		if err != nil {
			return nil, errorhandler.DefaultRepositoryError(l.Logger, err, in)
		}

		return data, nil
	}

	return nil, errorx.NewInvalidArgumentError(i18n.ValidationError)
}

func (l *BackendCreateRecordLogic) qiniuFetchWithoutKey(
	in *attachment.RecordInfo,
) (*model.AttachmentFileInfo, error) {
	if in.Url != nil && len(strings.TrimSpace(*in.Url)) > 0 {
		var url string = strings.TrimSpace(*in.Url)

		// 不指定保存的key，默认用文件hash作为文件名
		fetchRet, err := l.bucketManager.FetchWithoutKey(url, l.bucket)
		if err != nil {
			logrus.Info(fmt.Sprintf("Qiniu FetchWithoutKey err: %+v", err))
			return nil, errorx.NewInternalError("file.fetchFileFailed")
		}

		logrus.Info(fmt.Sprintf("Qiniu FetchWithoutKey: %+v", fetchRet))
		// {Hash:FpHyF0kkil3sp-SaXXX8TBJY3jDh Fsize:163469 MimeType:image/png Key:FpHyF0kkil3sp-SaXXX8TBJY3jDh}

		data, err := l.createOrUpdate(l.bucket, fetchRet.Key, nil)
		if err != nil {
			return nil, errorhandler.DefaultRepositoryError(l.Logger, err, in)
		}

		return data, nil
	}

	return nil, errorx.NewInvalidArgumentError(i18n.ValidationError)
}

func (l *BackendCreateRecordLogic) findAttachment(in *attachment.RecordInfo) (*model.Attachment, error) {
	if in.Id != nil && len(strings.TrimSpace(*in.Id)) > 0 {
		if i, err := strconv.ParseInt(strings.TrimSpace(*in.Id), 10, 64); err == nil && i > 0 {
			res, err := l.svcCtx.MediaFile.FindOneNoCache(l.ctx, nil, uint64(i))
			if err != nil {
				return nil, errorhandler.DefaultRepositoryError(l.Logger, err, in)
			}

			return res, nil
		}
	}

	return nil, errorx.NewInvalidArgumentError(i18n.ValidationError)
}

func (l *BackendCreateRecordLogic) findInfo(file *model.Attachment, key string, pid uint64) (*model.AttachmentFileInfo, error) {
	filterMap := make(map[string]map[string][]any)
	filterMap[model.AttachmentFileInfoColumns.KeyValue] = map[string][]any{
		"equalTo": {key},
	}
	filterMap[model.AttachmentFileInfoColumns.ParentID] = map[string][]any{
		"equalTo": {pid},
	}
	filterMap[model.AttachmentFileInfoColumns.DelStatus] = map[string][]any{
		"notEqualTo": {globalkey.StatusDeleted},
	}
	list, count, err := l.svcCtx.FileInfo.GetList(l.ctx, model.M{
		"pageSize": uint64(1),
		"orderBy":  model.AttachmentFileInfoColumns.ID + " DESC",
	}, 1, filterMap)
	if err != nil {
		return nil, err
	}

	var info *model.AttachmentFileInfo
	if count > 1 { // 记录重复异常
		info = list[0]

		var metaVals []string
		for _, fi := range list {
			metaVals = append(metaVals, strconv.FormatUint(fi.ID, 10))
		}

		var log model.AttachmentFileInfoLog
		log.AttachmentID = file.ID
		log.Type = uint8(globalkey.AttachmentLogInfoDuplicated)
		log.KeyValue = key
		log.Count = uint64(count)
		log.MetaValue = strings.Join(metaVals, ",")
		err = l.svcCtx.FileInfo.InsertLog(l.ctx, nil, &log)
		if err != nil {
			return nil, err
		}

		return info, nil
	}

	if count == 1 {
		info = list[0]

		if info.AttachmentID > 0 && info.AttachmentID != file.ID { // 记录重复异常
			var log model.AttachmentFileInfoLog
			log.AttachmentID = file.ID
			log.InfoID = info.ID
			log.Type = uint8(globalkey.AttachmentLogDuplicated)
			log.KeyValue = key
			log.Count = uint64(count)

			err = l.svcCtx.FileInfo.InsertLog(l.ctx, nil, &log)
			if err != nil {
				return nil, err
			}
		}

		return info, nil
	}

	return nil, nil
}

func (l *BackendCreateRecordLogic) findInfoByKey(key string, pid uint64) (*model.AttachmentFileInfo, error) {
	filterMap := make(map[string]map[string][]any)
	filterMap[model.AttachmentFileInfoColumns.KeyValue] = map[string][]any{
		"equalTo": {key},
	}
	filterMap[model.AttachmentFileInfoColumns.DelStatus] = map[string][]any{
		"notEqualTo": {globalkey.StatusDeleted},
	}
	if pid > 0 {
		filterMap[model.AttachmentFileInfoColumns.ParentID] = map[string][]any{
			"equalTo": {pid},
		}
	}
	list, _, err := l.svcCtx.FileInfo.GetList(l.ctx, model.M{
		"pageSize": uint64(1),
		"orderBy":  model.AttachmentFileInfoColumns.ID + " DESC",
	}, -1, filterMap)
	if err != nil {
		return nil, err
	}

	if len(list) == 1 {
		info := list[0]
		return info, nil
	}

	return nil, nil
}

func (l *BackendCreateRecordLogic) getAttachmentType(fileType int32, op string) int32 {
	var newFileType int32
	switch op {
	case "put":
		switch fileType {
		case globalkey.AttachmentTypePutPendingFile:
			newFileType = globalkey.AttachmentTypePutReadyFile
		case globalkey.AttachmentTypeWatermarkPendingFile:
			newFileType = globalkey.AttachmentTypeWatermarkPendingPutReadyFile
		case globalkey.AttachmentTypeThumbnailPendingFile:
			newFileType = globalkey.AttachmentTypeThumbnailPendingPutReadyFile
		case globalkey.AttachmentTypePendingFile:
			newFileType = globalkey.AttachmentTypePendingPutReadyFile
		default:
			newFileType = globalkey.AttachmentTypePutReadyFile
		}
	case "thumbnail":
		switch fileType {
		case globalkey.AttachmentTypeThumbnailPendingPutReadyFile:
			newFileType = globalkey.AttachmentTypeThumbnailReadyFile
		case globalkey.AttachmentTypePendingWatermarkReadyFile:
			newFileType = globalkey.AttachmentTypeReadyFile
		default:
			newFileType = globalkey.AttachmentTypeThumbnailReadyFile
		}
	case "watermark":
		switch fileType {
		case globalkey.AttachmentTypeWatermarkPendingPutReadyFile:
			newFileType = globalkey.AttachmentTypeWatermarkReadyFile
		case globalkey.AttachmentTypePendingPutReadyFile:
			newFileType = globalkey.AttachmentTypePendingWatermarkReadyFile
		default:
			newFileType = globalkey.AttachmentTypeWatermarkReadyFile
		}
	}

	return newFileType
}

func (l *BackendCreateRecordLogic) qiniuPutFile(in *attachment.RecordInfo) (*model.AttachmentFileInfo, error) {
	file, err := l.findAttachment(in)
	if err != nil {
		return nil, err
	}

	if file == nil {
		return nil, errorx.NewNotFoundError(i18n.TargetNotFound)
	}

	var destBucket string
	var destKey string
	if in.Bucket != nil && len(strings.TrimSpace(*in.Bucket)) > 0 {
		destBucket = strings.TrimSpace(*in.Bucket)
	}
	if len(destBucket) == 0 {
		destBucket = l.bucket
	}
	if in.Key != nil && len(strings.TrimSpace(*in.Key)) > 0 {
		destKey = strings.TrimSpace(*in.Key)
	}
	if len(destKey) == 0 {
		destKey = strings.TrimLeft(file.Path, l.svcCtx.Config.NfsConf.QiniuPrefix)
	}
	var localFile string = l.svcCtx.Config.NfsConf.PathPrefix + file.Path

	putPolicy := storage.PutPolicy{
		Scope: destBucket,
	}
	mac := qbox.NewMac(l.svcCtx.Config.QiniuConf.AccessKey, l.svcCtx.Config.QiniuConf.SecretKey)
	upToken := putPolicy.UploadToken(mac)

	// 构建表单上传的对象
	formUploader := storage.NewFormUploader(l.config)
	ret := storage.PutRet{}
	// 可选配置
	putExtra := storage.PutExtra{
		Params: map[string]string{
			"x:name": "Server Upload",
		},
	}

	err = formUploader.PutFile(context.Background(), &ret, upToken, destKey, localFile, &putExtra)
	if err != nil {
		logrus.Info(fmt.Sprintf("Qiniu Put err: %+v", err))
		return nil, errorx.NewInternalError("file.updateFileFailed")
	}

	// logrus.Info(fmt.Sprintf("Qiniu Put: %+v", ret))
	// {Hash:FlsEzctYF6lTDhazCZ4OkOyE65MI PersistentID: Key:uploads/image/202310/018b1311-f7a1-7204-aa33-7ca964d50185.jpg}

	info, err := l.findInfo(file, ret.Key, 0)
	if err != nil {
		return nil, errorhandler.DefaultRepositoryError(l.Logger, err, in)
	}

	if info != nil {
		if info.AttachmentID == 0 { // 关联操作
			info.AttachmentID = file.ID
			err = l.svcCtx.FileInfo.Update(l.ctx, nil, info)
			if err != nil {
				return nil, errorhandler.DefaultRepositoryError(l.Logger, err, in)
			}
		}
	} else {
		var newInfo model.AttachmentFileInfo
		newInfo.AttachmentID = file.ID
		newInfo.KeyValue = ret.Key
		newInfo.Hash = ret.Hash
		newInfo.PersistentID = ret.PersistentID
		res, err := l.createOrUpdate(destBucket, destKey, &newInfo)
		if err != nil {
			return nil, errorhandler.DefaultRepositoryError(l.Logger, err, in)
		}

		info = res
	}

	newType := l.getAttachmentType(int32(file.PostType), "put")
	if len(file.GUID) == 0 || newType > 0 {
		if len(file.GUID) == 0 {
			file.GUID = "/" + ret.Key
		}
		if newType > 0 {
			file.PostType = uint16(newType)
		}
		err = l.svcCtx.MediaFile.Update(l.ctx, nil, file, nil)
		if err != nil {
			return nil, errorhandler.DefaultRepositoryError(l.Logger, err, in)
		}
	}

	return info, nil
}

func (l *BackendCreateRecordLogic) qiniuPut(in *attachment.RecordInfo) (*model.AttachmentFileInfo, error) {
	file, err := l.findAttachment(in)
	if err != nil {
		return nil, err
	}

	if file == nil {
		return nil, errorx.NewNotFoundError(i18n.TargetNotFound)
	}

	var destBucket string
	var destKey string
	if in.Bucket != nil && len(strings.TrimSpace(*in.Bucket)) > 0 {
		destBucket = strings.TrimSpace(*in.Bucket)
	}
	if len(destBucket) == 0 {
		destBucket = l.bucket
	}
	if in.Key != nil && len(strings.TrimSpace(*in.Key)) > 0 {
		destKey = strings.TrimSpace(*in.Key)
	}
	if len(destKey) == 0 {
		destKey = strings.TrimLeft(file.Path, l.svcCtx.Config.NfsConf.QiniuPrefix)
	}

	putPolicy := storage.PutPolicy{
		Scope: destBucket,
	}
	mac := qbox.NewMac(l.svcCtx.Config.QiniuConf.AccessKey, l.svcCtx.Config.QiniuConf.SecretKey)
	upToken := putPolicy.UploadToken(mac)

	// 构建表单上传的对象
	formUploader := storage.NewFormUploader(l.config)
	ret := storage.PutRet{}
	// 可选配置
	putExtra := storage.PutExtra{
		Params: map[string]string{
			"x:name": "Server Upload",
		},
	}
	data := []byte("hello, this is qiniu cloud")
	dataLen := int64(len(data))

	err = formUploader.Put(context.Background(), &ret, upToken, destKey, bytes.NewReader(data), dataLen, &putExtra)
	if err != nil {
		logrus.Info(fmt.Sprintf("Qiniu Put err: %+v", err))
		return nil, err
	}

	logrus.Info(fmt.Sprintf("Qiniu Put: %+v", ret))
	// {Hash:FlsEzctYF6lTDhazCZ4OkOyE65MI PersistentID: Key:uploads/image/202310/018b1311-f7a1-7204-aa33-7ca964d50185.jpg}

	return nil, nil
}

func (l *BackendCreateRecordLogic) qiniuResumePut(in *attachment.RecordInfo) (*model.AttachmentFileInfo, error) {
	file, err := l.findAttachment(in)
	if err != nil {
		return nil, err
	}

	if file == nil {
		return nil, errorx.NewNotFoundError(i18n.TargetNotFound)
	}

	var destBucket string
	var destKey string
	if in.Bucket != nil && len(strings.TrimSpace(*in.Bucket)) > 0 {
		destBucket = strings.TrimSpace(*in.Bucket)
	}
	if len(destBucket) == 0 {
		destBucket = l.bucket
	}
	if in.Key != nil && len(strings.TrimSpace(*in.Key)) > 0 {
		destKey = strings.TrimSpace(*in.Key)
	}
	if len(destKey) == 0 {
		destKey = strings.TrimLeft(file.Path, l.svcCtx.Config.NfsConf.QiniuPrefix)
	}
	var localFile string = l.svcCtx.Config.NfsConf.PathPrefix + file.Path

	putPolicy := storage.PutPolicy{
		Scope: destBucket,
	}
	mac := qbox.NewMac(l.svcCtx.Config.QiniuConf.AccessKey, l.svcCtx.Config.QiniuConf.SecretKey)
	upToken := putPolicy.UploadToken(mac)

	// 构建表单上传的对象
	resumeUploader := storage.NewResumeUploaderV2(l.config)
	ret := storage.PutRet{}
	// 可选配置
	putExtra := storage.RputV2Extra{}

	err = resumeUploader.PutFile(context.Background(), &ret, upToken, destKey, localFile, &putExtra)
	if err != nil {
		logrus.Info(fmt.Sprintf("Qiniu ResumeUploaderV2 PutFile err: %+v", err))
		return nil, err
	}

	logrus.Info(fmt.Sprintf("Qiniu ResumeUploaderV2 PutFile: %+v", ret))
	//

	return nil, nil
}

func (l *BackendCreateRecordLogic) qiniuResumePutFile(in *attachment.RecordInfo) (*model.AttachmentFileInfo, error) {
	file, err := l.findAttachment(in)
	if err != nil {
		return nil, err
	}

	if file == nil {
		return nil, errorx.NewNotFoundError(i18n.TargetNotFound)
	}

	var destBucket string
	var destKey string
	if in.Bucket != nil && len(strings.TrimSpace(*in.Bucket)) > 0 {
		destBucket = strings.TrimSpace(*in.Bucket)
	}
	if len(destBucket) == 0 {
		destBucket = l.bucket
	}
	if in.Key != nil && len(strings.TrimSpace(*in.Key)) > 0 {
		destKey = strings.TrimSpace(*in.Key)
	}
	if len(destKey) == 0 {
		destKey = strings.TrimLeft(file.Path, l.svcCtx.Config.NfsConf.QiniuPrefix)
	}
	var localFile string = l.svcCtx.Config.NfsConf.PathPrefix + file.Path

	putPolicy := storage.PutPolicy{
		Scope: destBucket,
	}
	mac := qbox.NewMac(l.svcCtx.Config.QiniuConf.AccessKey, l.svcCtx.Config.QiniuConf.SecretKey)
	upToken := putPolicy.UploadToken(mac)

	// 构建表单上传的对象
	resumeUploader := storage.NewResumeUploaderV2(l.config)
	ret := storage.PutRet{}
	recorder, err := storage.NewFileRecorder(os.TempDir())
	if err != nil {
		logrus.Info(fmt.Sprintf("Qiniu ResumeUploaderV2 FileRecorder err: %+v", err))
		return nil, err
	}
	// 可选配置
	putExtra := storage.RputV2Extra{
		Recorder: recorder,
	}

	err = resumeUploader.PutFile(context.Background(), &ret, upToken, destKey, localFile, &putExtra)
	if err != nil {
		logrus.Info(fmt.Sprintf("Qiniu ResumeUploaderV2 Recorder PutFile err: %+v", err))
		return nil, err
	}

	logrus.Info(fmt.Sprintf("Qiniu ResumeUploaderV2 Recorder PutFile: %+v", ret))
	//

	return nil, nil
}

// https://developer.qiniu.com/dora/1279/basic-processing-images-imageview2
func (l *BackendCreateRecordLogic) qiniuPfopThumbnail(in *attachment.RecordInfo) (*model.AttachmentFileInfo, error) {
	file, err := l.findAttachment(in)
	if err != nil {
		return nil, err
	}

	if file == nil {
		return nil, errorx.NewNotFoundError(i18n.TargetNotFound)
	}

	if len(file.GUID) == 0 {
		return nil, errorx.NewInternalError("file.wrongKey")
	}

	info, err := l.findInfo(file, strings.TrimLeft(file.GUID, "/"), 0)
	if err != nil {
		return nil, errorhandler.DefaultRepositoryError(l.Logger, err, in)
	}

	if info == nil {
		return nil, errorx.NewNotFoundError(i18n.TargetNotFound)
	}

	if len(info.Bucket) > 0 {
		l.bucket = info.Bucket
	}

	var interlace string
	if file.PostMimeType == "image/jpeg" || file.PostMimeType == "image/jpg" {
		interlace = "/interlace/1"
	}

	dotIndex := strings.LastIndex(file.GUID, ".")
	slashIndex := strings.LastIndex(file.GUID, "/")
	var keyName string
	var keySuffix string
	if dotIndex == -1 || dotIndex < slashIndex {
		keyName = strings.TrimLeft(file.GUID, "/")
	} else {
		keyName = strings.TrimLeft(file.GUID[:dotIndex], "/")
		keySuffix = file.GUID[dotIndex:]
	}

	var style string
	if in.Style != nil && len(strings.TrimSpace(*in.Style)) > 0 {
		style = strings.TrimSpace(*in.Style)
	}

	// 使用数据处理，首先需要构建一个OperationManager对象
	mac := qbox.NewMac(l.svcCtx.Config.QiniuConf.AccessKey, l.svcCtx.Config.QiniuConf.SecretKey)
	operationManager := storage.NewOperationManager(mac, l.config)

	var fopThumbnail string
	var thumbnailKey string
	switch style {
	case "tiny":
		fopThumbnail = "imageView2/0/w/96"
		thumbnailKey = fmt.Sprintf("%s_96%s", keyName, keySuffix)
	case "thumbnail":
		fopThumbnail = "imageView2/0/w/320"
		thumbnailKey = fmt.Sprintf("%s_320%s", keyName, keySuffix)
	default:
		fopThumbnail = "imageView2/0/w/320"
		thumbnailKey = fmt.Sprintf("%s_320%s", keyName, keySuffix)
	}

	var encodedEntryURI string = storage.EncodedEntry(l.bucket, thumbnailKey)

	// 处理指令集合
	// fopAvthumb := fmt.Sprintf("avthumb/mp4/s/480x320/vb/500k|saveas/%s",
	// 	storage.EncodedEntry(saveBucket, "pfop_test_qiniu.mp4"))
	// fopVframe := fmt.Sprintf("vframe/jpg/offset/10|saveas/%s",
	// 	storage.EncodedEntry(saveBucket, "pfop_test_qiniu.jpg"))
	// fopVsample := fmt.Sprintf("vsample/jpg/interval/20/pattern/%s",
	// 	base64.URLEncoding.EncodeToString([]byte("pfop_test_$(count).jpg")))
	// fopBatch := []string{fopAvthumb, fopVframe, fopVsample}
	// fopVframe := fmt.Sprintf("imageMogr2/thumbnail/200x200!|saveas/%s",
	// 	storage.EncodedEntry(saveBucket, "pfop_test_qiniu.jpg"))

	var fopBatch []string
	fopImageView := fmt.Sprintf("%s%s|saveas/%s", fopThumbnail, interlace, encodedEntryURI)
	fopBatch = append(fopBatch, fopImageView)

	fops := strings.Join(fopBatch, ";")
	// 强制重新执行数据处理任务
	force := true
	// 数据处理指令全部完成之后，通知该地址
	// notifyURL := "http://106.52.221.227:8999/v1/pfop/callback"
	notifyURL := l.svcCtx.Config.QiniuConf.NotifyURL
	// 数据处理的私有队列，必须指定以保障处理速度
	pipeline := "scentrobe_private"
	persistentId, err := operationManager.Pfop(l.bucket, strings.TrimLeft(file.GUID, "/"), fops, pipeline, notifyURL, force)
	if err != nil {
		logrus.Info(fmt.Sprintf("Qiniu Pfop err: %+v", err))
		return nil, errorx.NewInternalError("file.updateFileFailed")
	}

	// logrus.Info(fmt.Sprintf("Qiniu Pfop persistentId: %+v", persistentId))
	// Qiniu Pfop persistentId: z2.01z211cwe14h24zxvs00mv2nrn000d7n

	thumbnailInfo, err := l.findInfo(file, thumbnailKey, info.ID)
	if err != nil {
		return nil, errorhandler.DefaultRepositoryError(l.Logger, err, in)
	}

	if thumbnailInfo != nil {
		thumbnailInfo.PersistentID = persistentId
		err = l.svcCtx.FileInfo.Update(l.ctx, nil, thumbnailInfo)
		if err != nil {
			return nil, errorhandler.DefaultRepositoryError(l.Logger, err, in)
		}
	} else {
		var newInfo model.AttachmentFileInfo
		newInfo.AttachmentID = file.ID
		newInfo.ParentID = info.ID
		newInfo.KeyValue = thumbnailKey
		newInfo.PersistentID = persistentId
		newInfo.FileType = uint8(globalkey.AttachmentTypeThumbnail)
		err = l.svcCtx.FileInfo.Insert(l.ctx, nil, &newInfo)
		if err != nil {
			return nil, errorhandler.DefaultRepositoryError(l.Logger, err, in)
		}

		thumbnailInfo = &newInfo
	}

	newType := l.getAttachmentType(int32(file.PostType), "thumbnail")
	if newType > 0 {
		file.PostType = uint16(newType)
		err = l.svcCtx.MediaFile.Update(l.ctx, nil, file, nil)
		if err != nil {
			return nil, errorhandler.DefaultRepositoryError(l.Logger, err, in)
		}
	}

	return thumbnailInfo, nil
}

func (l *BackendCreateRecordLogic) qiniuPfopWatermark(in *attachment.RecordInfo) (*model.AttachmentFileInfo, error) {
	file, err := l.findAttachment(in)
	if err != nil {
		return nil, err
	}

	if file == nil {
		return nil, errorx.NewNotFoundError(i18n.TargetNotFound)
	}

	if len(file.GUID) == 0 {
		return nil, errorx.NewInternalError("file.wrongKey")
	}

	info, err := l.findInfo(file, strings.TrimLeft(file.GUID, "/"), 0)
	if err != nil {
		return nil, errorhandler.DefaultRepositoryError(l.Logger, err, in)
	}

	if info == nil {
		return nil, errorx.NewNotFoundError(i18n.TargetNotFound)
	}

	if len(info.Bucket) > 0 {
		l.bucket = info.Bucket
	}

	var encodedKodoScheme string
	if in.FileId != nil && *in.FileId > 0 {
		watermark, err := l.svcCtx.MediaFile.FindOneNoCache(l.ctx, nil, *in.FileId)
		if err != nil {
			logx.Errorw("failed to find the specific watermark", logx.Field("detail", err.Error()))
		} else {
			watermarkInfo, err := l.findInfo(watermark, strings.TrimLeft(watermark.GUID, "/"), 0)
			if err != nil {
				logx.Errorw("failed to find the specific watermark info", logx.Field("detail", err.Error()))
			} else if watermarkInfo == nil {
				logx.Errorw("failed to find the specific watermark info, not found")
			} else if watermarkInfo.Bucket != l.bucket {
				logx.Errorw("failed to find the specific watermark info, different bucket")
			} else {
				encodedKodoScheme = l.encodedKodoEntry(watermarkInfo.KeyValue)
			}
		}
	}
	if len(encodedKodoScheme) == 0 {
		// encodedKodoScheme = l.encodedKodoEntry("logo.png")
		encodedKodoScheme = "a29kbzovL2R1amlhb3hpdS9sb2dvLnBuZw=="
	}
	// logrus.Info(fmt.Sprintf("qiniuPfopWatermark encodedKodoScheme: %+v", encodedKodoScheme))
	// a29kbzovL2R1amlhb3hpdS9sb2dvLnBuZw==

	dotIndex := strings.LastIndex(file.GUID, ".")
	slashIndex := strings.LastIndex(file.GUID, "/")
	var keyName string
	var keySuffix string
	if dotIndex == -1 || dotIndex < slashIndex {
		keyName = strings.TrimLeft(file.GUID, "/")
	} else {
		keyName = strings.TrimLeft(file.GUID[:dotIndex], "/")
		keySuffix = file.GUID[dotIndex:]
	}

	var style string
	if in.Style != nil && len(strings.TrimSpace(*in.Style)) > 0 {
		style = strings.TrimSpace(*in.Style)
	}

	// 使用数据处理，首先需要构建一个OperationManager对象
	mac := qbox.NewMac(l.svcCtx.Config.QiniuConf.AccessKey, l.svcCtx.Config.QiniuConf.SecretKey)
	operationManager := storage.NewOperationManager(mac, l.config)

	var mode int = 1                 // 图片水印
	var dissolve int = 70            // 透明度，取值范围1-100，默认值为100（完全不透明）。
	var gravity string = "NorthWest" // 水印位置，参考水印锚点参数表，默认值为SouthEast（右下角）。
	var dx int = 5                   // 横轴边距，单位:像素(px)，默认值为10。
	var dy int = 5                   // 纵轴边距，单位:像素(px)，默认值为10。
	var ws string = "0.2"            // 水印图片自适应原图的比例，ws的取值范围为0-1。具体是指水印图片保持原比例，按照自适应原图的类型wst，比如默认是按照短边，则水印图片短边=原图短边＊ws。
	// var wst int = 0 // 水印图片自适应原图的类型，取值0、1、2、3分别表示为自适应原图的短边、长边、宽、高，默认值为0。
	var fopWatermark string
	var watermarkKey string
	switch style {
	default:
		fopWatermark = fmt.Sprintf("watermark/%d/image/%s/dissolve/%d/gravity/%s/dx/%d/dy/%d/ws/%s",
			mode, encodedKodoScheme, dissolve, gravity, dx, dy, ws)
		// /wst/%d
		watermarkKey = fmt.Sprintf("%s_wm%s", keyName, keySuffix)
	}

	// logrus.Info(fmt.Sprintf("Qiniu Pfop fopWatermark: %s", fopWatermark))

	var encodedEntryURI string = storage.EncodedEntry(l.bucket, watermarkKey)

	var fopBatch []string
	fopImageView := fmt.Sprintf("%s|saveas/%s", fopWatermark, encodedEntryURI)
	fopBatch = append(fopBatch, fopImageView)

	fops := strings.Join(fopBatch, ";")
	// 强制重新执行数据处理任务
	force := true
	// 数据处理指令全部完成之后，通知该地址
	notifyURL := l.svcCtx.Config.QiniuConf.NotifyURL
	// notifyURL := ""
	// 数据处理的私有队列，必须指定以保障处理速度
	pipeline := "scentrobe_private"
	persistentId, err := operationManager.Pfop(l.bucket, strings.TrimLeft(file.GUID, "/"), fops, pipeline, notifyURL, force)
	if err != nil {
		logrus.Info(fmt.Sprintf("Qiniu Pfop err: %+v", err))
		return nil, errorx.NewInternalError("file.updateFileFailed")
	}

	// logrus.Info(fmt.Sprintf("Qiniu Pfop persistentId: %+v", persistentId))
	// Qiniu Pfop persistentId: z2.01z211cwe14h24zxvs00mv2nrn000d7n

	watermarkInfo, err := l.findInfo(file, watermarkKey, info.ID)
	if err != nil {
		return nil, errorhandler.DefaultRepositoryError(l.Logger, err, in)
	}

	if watermarkInfo != nil {
		watermarkInfo.PersistentID = persistentId
		err = l.svcCtx.FileInfo.Update(l.ctx, nil, watermarkInfo)
		if err != nil {
			return nil, errorhandler.DefaultRepositoryError(l.Logger, err, in)
		}
	} else {
		var newInfo model.AttachmentFileInfo
		newInfo.AttachmentID = file.ID
		newInfo.ParentID = info.ID
		newInfo.KeyValue = watermarkKey
		newInfo.PersistentID = persistentId
		newInfo.FileType = uint8(globalkey.AttachmentTypeWatermark)
		err = l.svcCtx.FileInfo.Insert(l.ctx, nil, &newInfo)
		if err != nil {
			return nil, errorhandler.DefaultRepositoryError(l.Logger, err, in)
		}

		watermarkInfo = &newInfo
	}

	newType := l.getAttachmentType(int32(file.PostType), "watermark")
	if newType > 0 {
		file.PostType = uint16(newType)
		err = l.svcCtx.MediaFile.Update(l.ctx, nil, file, nil)
		if err != nil {
			return nil, errorhandler.DefaultRepositoryError(l.Logger, err, in)
		}
	}

	return watermarkInfo, nil
}

// encodedKodoEntry 生成URL Safe Base64编码的 kodo 资源 Entry
func (l *BackendCreateRecordLogic) encodedKodoEntry(key string) string {
	entry := fmt.Sprintf("kodo://%s/%s", l.bucket, key)
	return base64.URLEncoding.EncodeToString([]byte(entry))
}

// 查询数据处理请求状态
// 由于数据处理是异步处理，可以根据发送处理请求时返回的 persistentId 去查询任务的处理进度，如果在上传策略里面设置了persistentNotifyUrl或者发送处理请求时指定notifyURL的情况下，直接业务服务器等待处理结果通知即可，如果需要主动查询，可以采用如下代码中的：
func (l *BackendCreateRecordLogic) qiniuPrefop(in *attachment.RecordInfo) (*model.AttachmentFileInfo, error) {
	if in.Id != nil && len(strings.TrimSpace(*in.Id)) > 0 {
		// 使用数据处理，首先需要构建一个OperationManager对象
		mac := qbox.NewMac(l.svcCtx.Config.QiniuConf.AccessKey, l.svcCtx.Config.QiniuConf.SecretKey)
		operationManager := storage.NewOperationManager(mac, l.config)

		ret, err := operationManager.Prefop(*in.Id)
		if err != nil {
			logrus.Info(fmt.Sprintf("Qiniu Prefop err: %+v", err))
			// Qiniu Prefop err: no such file or directory
			return nil, err
		}

		logrus.Info(fmt.Sprintf("Qiniu Prefop: %+v", ret))
		// return ret.String(), nil
	}

	return nil, nil
}

func (l *BackendCreateRecordLogic) qiniuPfopCallback(in *attachment.RecordInfo) (*model.AttachmentFileInfo, error) {
	if in.Key != nil && len(strings.TrimSpace(*in.Key)) > 0 && in.Style != nil && len(strings.TrimSpace(*in.Style)) > 0 {
		var persistentId string
		if in.Id != nil && len(strings.TrimSpace(*in.Id)) > 0 {
			persistentId = strings.TrimSpace(*in.Id)
		}
		var bucket string
		if in.Bucket != nil && len(strings.TrimSpace(*in.Bucket)) > 0 {
			bucket = strings.TrimSpace(*in.Bucket)
		} else {
			bucket = l.bucket
		}
		info, err := l.findInfoByKey(strings.TrimSpace(*in.Key), 0)
		if err != nil {
			return nil, errorhandler.DefaultRepositoryError(l.Logger, err, in)
		}

		if keys := strings.Split(strings.TrimSpace(*in.Style), ","); len(keys) > 0 {
			for _, key := range keys {
				fopResultInfo, err := l.findInfoByKey(strings.TrimSpace(key), info.ID)
				if err != nil {
					return nil, errorhandler.DefaultRepositoryError(l.Logger, err, in)
				}

				if fopResultInfo == nil {
					var newInfo model.AttachmentFileInfo
					newInfo.AttachmentID = info.AttachmentID
					newInfo.ParentID = info.ID
					newInfo.PersistentID = persistentId
					_, err = l.createOrUpdate(bucket, key, &newInfo)
					if err != nil {
						return nil, errorhandler.DefaultRepositoryError(l.Logger, err, in)
					}
				} else if fopResultInfo.PersistentID == persistentId {
					_, err = l.createOrUpdate(bucket, key, fopResultInfo)
					if err != nil {
						return nil, errorhandler.DefaultRepositoryError(l.Logger, err, in)
					}
				}
			}
		}
	}

	return nil, nil
}
